/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.tree;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.dom4j.DocumentFactory;
import org.dom4j.Namespace;
import org.dom4j.QName;

/**
 * NamespaceStack implements a stack of namespaces and optionally maintains a
 * cache of all the fully qualified names (<code>QName</code>) which are in
 * scope. This is useful when building or navigating a <i>dom4j </i> document.
 * 
 * @author <a href="mailto:jstrachan@apache.org">James Strachan </a>
 * @version $Revision: 1.2 $
 */
public class NamespaceStack
{
   /** The factory used to create new <code>Namespace</code> instances */
   private DocumentFactory documentFactory;

   /** The Stack of namespaces */
   private ArrayList namespaceStack = new ArrayList();

   /** The cache of qualifiedNames to QNames per namespace context */
   private ArrayList namespaceCacheList = new ArrayList();

   /**
    * A cache of current namespace context cache of mapping from qualifiedName
    * to QName
    */
   private Map currentNamespaceCache;

   /**
    * A cache of mapping from qualifiedName to QName before any namespaces are
    * declared
    */
   private Map rootNamespaceCache = new HashMap();

   /** Caches the default namespace defined via xmlns="" */
   private Namespace defaultNamespace;

   public NamespaceStack()
   {
      this.documentFactory = DocumentFactory.getInstance();
   }

   public NamespaceStack(DocumentFactory documentFactory)
   {
      this.documentFactory = documentFactory;
   }

   /**
    * Pushes the given namespace onto the stack so that its prefix becomes
    * available.
    * 
    * @param namespace is the <code>Namespace</code> to add to the stack.
    */
   public void push(Namespace namespace)
   {
      namespaceStack.add(namespace);
      namespaceCacheList.add(null);
      currentNamespaceCache = null;

      String prefix = namespace.getPrefix();

      if((prefix == null) || (prefix.length() == 0))
      {
         defaultNamespace = namespace;
      }
   }

   /**
    * Pops the most recently used <code>Namespace</code> from the stack
    * 
    * @return Namespace popped from the stack
    */
   public Namespace pop()
   {
      return remove(namespaceStack.size() - 1);
   }

   /**
    * DOCUMENT ME!
    * 
    * @return the number of namespaces on the stackce stack.
    */
   public int size()
   {
      return namespaceStack.size();
   }

   /**
    * Clears the stack
    */
   public void clear()
   {
      namespaceStack.clear();
      namespaceCacheList.clear();
      rootNamespaceCache.clear();
      currentNamespaceCache = null;
   }

   /**
    * DOCUMENT ME!
    * 
    * @param index DOCUMENT ME!
    * 
    * @return the namespace at the specified index on the stack
    */
   public Namespace getNamespace(int index)
   {
      return (Namespace) namespaceStack.get(index);
   }

   /**
    * DOCUMENT ME!
    * 
    * @param prefix DOCUMENT ME!
    * 
    * @return the namespace for the given prefix or null if it could not be
    *         found.
    */
   public Namespace getNamespaceForPrefix(String prefix)
   {
      if(prefix == null)
      {
         prefix = "";
      }

      for(int i = namespaceStack.size() - 1; i >= 0; i--)
      {
         Namespace namespace = (Namespace) namespaceStack.get(i);

         if(prefix.equals(namespace.getPrefix()))
         {
            return namespace;
         }
      }

      return null;
   }

   /**
    * DOCUMENT ME!
    * 
    * @param prefix DOCUMENT ME!
    * 
    * @return the URI for the given prefix or null if it could not be found.
    */
   public String getURI(String prefix)
   {
      Namespace namespace = getNamespaceForPrefix(prefix);

      return (namespace != null) ? namespace.getURI() : null;
   }

   /**
    * DOCUMENT ME!
    * 
    * @param namespace DOCUMENT ME!
    * 
    * @return true if the given prefix is in the stack.
    */
   public boolean contains(Namespace namespace)
   {
      String prefix = namespace.getPrefix();
      Namespace current = null;

      if((prefix == null) || (prefix.length() == 0))
      {
         current = getDefaultNamespace();
      }
      else
      {
         current = getNamespaceForPrefix(prefix);
      }

      if(current == null)
      {
         return false;
      }

      if(current == namespace)
      {
         return true;
      }

      return namespace.getURI().equals(current.getURI());
   }

   public QName getQName(String namespaceURI, String localName,
      String qualifiedName)
   {
      if(localName == null)
      {
         localName = qualifiedName;
      }
      else if(qualifiedName == null)
      {
         qualifiedName = localName;
      }

      if(namespaceURI == null)
      {
         namespaceURI = "";
      }

      String prefix = "";
      int index = qualifiedName.indexOf(":");

      if(index > 0)
      {
         prefix = qualifiedName.substring(0, index);

         if(localName.trim().length() == 0)
         {
            localName = qualifiedName.substring(index + 1);
         }
      }
      else if(localName.trim().length() == 0)
      {
         localName = qualifiedName;
      }

      Namespace namespace = createNamespace(prefix, namespaceURI);

      return pushQName(localName, qualifiedName, namespace, prefix);
   }

   public QName getAttributeQName(String namespaceURI, String localName,
      String qualifiedName)
   {
      if(qualifiedName == null)
      {
         qualifiedName = localName;
      }

      Map map = getNamespaceCache();
      QName answer = (QName) map.get(qualifiedName);

      if(answer != null)
      {
         return answer;
      }

      if(localName == null)
      {
         localName = qualifiedName;
      }

      if(namespaceURI == null)
      {
         namespaceURI = "";
      }

      Namespace namespace = null;
      String prefix = "";
      int index = qualifiedName.indexOf(":");

      if(index > 0)
      {
         prefix = qualifiedName.substring(0, index);
         namespace = createNamespace(prefix, namespaceURI);

         if(localName.trim().length() == 0)
         {
            localName = qualifiedName.substring(index + 1);
         }
      }
      else
      {
         // attributes with no prefix have no namespace
         namespace = Namespace.NO_NAMESPACE;

         if(localName.trim().length() == 0)
         {
            localName = qualifiedName;
         }
      }

      answer = pushQName(localName, qualifiedName, namespace, prefix);
      map.put(qualifiedName, answer);

      return answer;
   }

   /**
    * Adds a namepace to the stack with the given prefix and URI
    * 
    * @param prefix DOCUMENT ME!
    * @param uri DOCUMENT ME!
    */
   public void push(String prefix, String uri)
   {
      if(uri == null)
      {
         uri = "";
      }

      Namespace namespace = createNamespace(prefix, uri);
      push(namespace);
   }

   /**
    * Adds a new namespace to the stack
    * 
    * @param prefix DOCUMENT ME!
    * @param uri DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   public Namespace addNamespace(String prefix, String uri)
   {
      Namespace namespace = createNamespace(prefix, uri);
      push(namespace);

      return namespace;
   }

   /**
    * Pops a namepace from the stack with the given prefix and URI
    * 
    * @param prefix DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   public Namespace pop(String prefix)
   {
      if(prefix == null)
      {
         prefix = "";
      }

      Namespace namespace = null;

      for(int i = namespaceStack.size() - 1; i >= 0; i--)
      {
         Namespace ns = (Namespace) namespaceStack.get(i);

         if(prefix.equals(ns.getPrefix()))
         {
            remove(i);
            namespace = ns;

            break;
         }
      }

      if(namespace == null)
      {
         System.out.println("Warning: missing namespace prefix ignored: "
            + prefix);
      }

      return namespace;
   }

   public String toString()
   {
      return super.toString() + " Stack: " + namespaceStack.toString();
   }

   public DocumentFactory getDocumentFactory()
   {
      return documentFactory;
   }

   public void setDocumentFactory(DocumentFactory documentFactory)
   {
      this.documentFactory = documentFactory;
   }

   public Namespace getDefaultNamespace()
   {
      if(defaultNamespace == null)
      {
         defaultNamespace = findDefaultNamespace();
      }

      return defaultNamespace;
   }

   // Implementation methods
   // -------------------------------------------------------------------------

   /**
    * Adds the QName to the stack of available QNames
    * 
    * @param localName DOCUMENT ME!
    * @param qualifiedName DOCUMENT ME!
    * @param namespace DOCUMENT ME!
    * @param prefix DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   protected QName pushQName(String localName, String qualifiedName,
      Namespace namespace, String prefix)
   {
      if((prefix == null) || (prefix.length() == 0))
      {
         this.defaultNamespace = null;
      }

      return createQName(localName, qualifiedName, namespace);
   }

   /**
    * Factory method to creeate new QName instances. By default this method
    * interns the QName
    * 
    * @param localName DOCUMENT ME!
    * @param qualifiedName DOCUMENT ME!
    * @param namespace DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   protected QName createQName(String localName, String qualifiedName,
      Namespace namespace)
   {
      return documentFactory.createQName(localName, namespace);
   }

   /**
    * Factory method to creeate new Namespace instances. By default this method
    * interns the Namespace
    * 
    * @param prefix DOCUMENT ME!
    * @param namespaceURI DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   protected Namespace createNamespace(String prefix, String namespaceURI)
   {
      return documentFactory.createNamespace(prefix, namespaceURI);
   }

   /**
    * Attempts to find the current default namespace on the stack right now or
    * returns null if one could not be found
    * 
    * @return DOCUMENT ME!
    */
   protected Namespace findDefaultNamespace()
   {
      for(int i = namespaceStack.size() - 1; i >= 0; i--)
      {
         Namespace namespace = (Namespace) namespaceStack.get(i);

         if(namespace != null)
         {
            String prefix = namespace.getPrefix();

            if((prefix == null) || (namespace.getPrefix().length() == 0))
            {
               return namespace;
            }
         }
      }

      return null;
   }

   /**
    * Removes the namespace at the given index of the stack
    * 
    * @param index DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   protected Namespace remove(int index)
   {
      Namespace namespace = (Namespace) namespaceStack.remove(index);
      namespaceCacheList.remove(index);
      defaultNamespace = null;
      currentNamespaceCache = null;

      return namespace;
   }

   protected Map getNamespaceCache()
   {
      if(currentNamespaceCache == null)
      {
         int index = namespaceStack.size() - 1;

         if(index < 0)
         {
            currentNamespaceCache = rootNamespaceCache;
         }
         else
         {
            currentNamespaceCache = (Map) namespaceCacheList.get(index);

            if(currentNamespaceCache == null)
            {
               currentNamespaceCache = new HashMap();
               namespaceCacheList.set(index, currentNamespaceCache);
            }
         }
      }

      return currentNamespaceCache;
   }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
